package it.unibo.refolding.pico.stripchart;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.DoubleIntPair;
import it.unibo.refolding.common.DoublePair;
import it.unibo.refolding.common.Images;
import it.unibo.refolding.pico.ApproachRetractionLoop;
import it.unibo.refolding.pico.ApproachRetractionPush;
import it.unibo.refolding.pico.ContactPointAndSlopeHelper;
import it.unibo.refolding.pico.FileMovingWatcher.FileContentAndTargetFile;
import it.unibo.refolding.pico.PicoConstants;
import it.unibo.refolding.pico.PicoData;
import it.unibo.refolding.pico.PicoDataAnalyser;
import it.unibo.refolding.pico.PicoDataAnalyser.ContactPointsAndSlope;
import it.unibo.refolding.pico.PicoDataAnalyser.PeakAnalysisInput;
import it.unibo.refolding.pico.PicoDataExtractor;
import it.unibo.refolding.pico.PicoShapes;
import it.unibo.refolding.pico.PlainPicoData;
import it.unibo.refolding.pico.RedundantPicoCurveInfo;
import it.unibo.refolding.pico.RefoldingResult;
import it.unibo.refolding.pico.RefoldingUtil;
import it.unibo.refolding.pico.RefoldingUtil.RefoldingCurve;
import it.unibo.refolding.pico.StripChartPicoData;
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
import it.unimi.dsi.fastutil.doubles.DoubleList;
import it.unimi.dsi.fastutil.doubles.DoubleLists;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntLists;

import java.awt.Color;
import java.awt.Shape;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.jfree.util.ShapeUtilities;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

public class StripChartRefolding {

  private static final Logger logger = Logger.getLogger(StripChartRefolding.class.getSimpleName());

  public static ApproachRetractionLoop stripChartToFetchRefoldBreak(
    StripChartPicoData stripChart, FetchRefoldBreakParams params, DoubleList allowedTimeRelativeError) {
    IntList a = findJunctionPoints(stripChart, params, allowedTimeRelativeError);
    return a == null ? null : createLoopGivenFetchRefoldBreakJunctionPoints(a, stripChart, params);
  }

  public static ApproachRetractionLoop createLoopGivenFetchRefoldBreakJunctionPoints(
    IntList junctionPoints, StripChartPicoData stripChart, FetchRefoldBreakParams params) {
    PlainPicoData[] iterations = {
    PicoDataExtractor.extractPlainPicoDataFromSegment(
      stripChart, params == null ? Double.NaN : params.fetchRampSizeM, params == null ? Double.NaN
        : params.velocityMPerS, params == null ? Double.NaN : params.fetchRampSizeM,
      params == null ? Double.NaN : params.velocityMPerS, junctionPoints.get(0), junctionPoints.get(1) + 1,
      junctionPoints.get(2), junctionPoints.get(3) + 1),
    PicoDataExtractor.extractPlainPicoDataFromSegment(
      stripChart, Double.NaN, PicoConstants.MAX_VELOCITY_M_PER_S, Double.NaN,
      params == null ? Double.NaN : params.velocityMPerS, junctionPoints.get(4), junctionPoints.get(5) + 1,
      junctionPoints.get(6), junctionPoints.get(7) + 1),};
    return new ApproachRetractionLoop(junctionPoints, iterations);
  }

  public static final int FETCH_REFOLD_BREAK_JUNCTIONS = 8;
  public static final DoubleList defaultAllowedTimeRelativeError = DoubleLists.unmodifiable(DoubleArrayList.wrap(
    new double[] {0.06, 0.04, 0.18, 0.4, 0.04, 0.3, 0.04}));

  public static IntList findJunctionPoints(
    StripChartPicoData stripChart, FetchRefoldBreakParams params, DoubleList allowedTimeRelativeError) {
    boolean sameRefoldingApproachVelocity = params.refoldingApproachVelocityMPerS == params.velocityMPerS;
    double[] slopes = new double[sameRefoldingApproachVelocity ? 3 : 4];
    slopes[1] = PicoDataExtractor.speedInZLsbPerPoint(stripChart, params.velocityMPerS);
    slopes[2] = PicoDataExtractor.speedInZLsbPerPoint(stripChart, -params.velocityMPerS);
    if (slopes.length == 4) {
      slopes[3] = PicoDataExtractor.speedInZLsbPerPoint(stripChart, params.refoldingApproachVelocityMPerS); 
    }
    DoubleIntPair[] timeAndSlopeIndices = {
    DoubleIntPair.of(params.fetchRampSizeM / params.velocityMPerS * stripChart.stripChartRateHz, 1),
    DoubleIntPair.of(params.fetchSurfaceDelayS * stripChart.stripChartRateHz, 0),
    DoubleIntPair.of(params.fetchRampSizeM / params.velocityMPerS * stripChart.stripChartRateHz, 2),
    DoubleIntPair.of(PicoConstants.DEAD_TIME_S * stripChart.stripChartRateHz, 0),
    DoubleIntPair.of(params.refoldingApproachM / params.refoldingApproachVelocityMPerS
      * stripChart.stripChartRateHz, sameRefoldingApproachVelocity ? 1 : 3),
    DoubleIntPair.of(params.refoldingSurfaceDelayS * stripChart.stripChartRateHz, 0),
    DoubleIntPair.of(
      params.breakRetractionM / params.velocityMPerS * stripChart.stripChartRateHz, 2),};
    Level level = Level.FINE;
    if (logger.isLoggable(level)) {
      logger.log(level, "timeAndSlopeIndices={0}", Arrays.toString(timeAndSlopeIndices));
    }
    double[] minR2 = new double[timeAndSlopeIndices.length];
    for (int i = minR2.length; --i >= 0;) {
      minR2[i] = timeAndSlopeIndices[i].y == 0 ? 0 : 0.999;
    }
    // Correlation of the almost perfectly vertical segment is a tricky one.
    if (params.refoldingApproachVelocityMPerS > 8e-6) {
      minR2[4] = 0.9;
    }
    CommonThings.squareInPlace(minR2);
    double[] atre = new double[timeAndSlopeIndices.length];
    Arrays.fill(atre, 0.04);
    for (int i = Math.min(atre.length, allowedTimeRelativeError.size()); --i >= 0; ) {
      atre[i] = allowedTimeRelativeError.getDouble(i);
    }
    PicoDataExtractor.JunctionPointsWithCost junctionPointsWithCost = PicoDataExtractor.matchSlopesWithDynProgramming(
      stripChart.sharedApproach().sharedZArray(), slopes, timeAndSlopeIndices,
      atre, minR2);
    logger.log(Level.FINE, String.format(
      "junctionPointsWithCost=%s fastVelocityMPerS=%g", junctionPointsWithCost,
      PicoConstants.MAX_VELOCITY_M_PER_S));
    int[] a;
    if (junctionPointsWithCost == null) {
      logger.warning("Strip chart doesn't seem to behave like a FetchRefoldBreak curve with parameters "
        + params);
      a = null;
    } else {
      a = junctionPointsWithCost.junctionPoints;
      Preconditions.checkArgument(FETCH_REFOLD_BREAK_JUNCTIONS == a.length);
    }
    return a == null ? null : IntLists.unmodifiable(IntArrayList.wrap(a));
  }

  public static class FetchRefoldBreakInfo {
    public final RedundantPicoCurveInfo fetchInfo;
    public final double approachPushingFromDeflectionM;
    public final double retractionPushingM;
    public final double hoverDistanceM;
    public final boolean proteinMightStillBeAttachedAfterFetch;
    public final int wellSpacedAtEndOfBreak;
    public final List<Shape> shapes;

    public FetchRefoldBreakInfo(RedundantPicoCurveInfo fetchInfo,
      double approachPushingFromDeflectionM, double retractionPushingM, double hoverDistanceM,
      boolean proteinMightStillBeAttachedAfterFetch, int wellSpacedAtEndOfBreak, List<Shape> shapes) {
      this.fetchInfo = fetchInfo;
      this.approachPushingFromDeflectionM = approachPushingFromDeflectionM;
      this.retractionPushingM = retractionPushingM;
      this.hoverDistanceM = hoverDistanceM;
      this.proteinMightStillBeAttachedAfterFetch = proteinMightStillBeAttachedAfterFetch;
      this.wellSpacedAtEndOfBreak = wellSpacedAtEndOfBreak;
      this.shapes = shapes;
    }

    public boolean isInteresting() {
      return wellSpacedAtEndOfBreak != 0;
    }

    public boolean isVeryInteresting() {
      return wellSpacedAtEndOfBreak > 1;
    }

    @Override
    public String toString() {
      return new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).setExcludeFieldNames(
        new String[] {"shapes"}).toString();
    }
  }

  public static FetchRefoldBreakInfo analyseFetchRefoldBreak(
    PeakAnalysisInput peakAnalysisInput, String filename, Path peakSlopesDir,
    ApproachRetractionPush approachRetractionPush, PlainPicoData refold,
    double minPeakForceRelativeToEmpiricalBaselineN, double medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    double rightMuchHigherThanAllN, double rightMuchHigherN, double atMostOnePeakWindowM,
    double clRangeStartM, double clRangeEndM, double linkerM, double linkerPerModuleM,
    boolean svgIfInteresting, double svgDefScale, DoublePair hoverZDef,
    int maxRequiredWellSpacedAtEnd, boolean debug) {
    final RedundantPicoCurveInfo fetchInfo = PicoDataAnalyser.analyseWithGivenContactPointsAndSlope(
      peakAnalysisInput, filename, peakSlopesDir);
    double hoverDistanceM = peakAnalysisInput.plainPicoData.zLsbToMetresFactor
      * (fetchInfo.retractionContactPoint.zLsb - hoverZDef.x);
    int minPeaks = RefoldingUtil.minPeaksForInterestingFetch(
      peakAnalysisInput.plainPicoData.rampSizeMetres(), approachRetractionPush.retractionPushingM,
      clRangeStartM, clRangeEndM, linkerM, linkerPerModuleM);
    minPeaks = Math.min(minPeaks, maxRequiredWellSpacedAtEnd);
    int allowSkipped = 1;
    boolean proteinMightStillBeAttached = PicoDataAnalyser.proteinMightStillBeAttached(
      fetchInfo, clRangeStartM, clRangeEndM, minPeaks, allowSkipped);
    RefoldingResult refoldingResult = new RefoldingResult(refold, null);
    boolean inadvertentlyPushing = RefoldingUtil.inadvertentlyPushing(
      refoldingResult, fetchInfo.retractionContactPoint.zLsb);
    final List<Shape> shapes;
    // TODO: We could do the analysis even without having detected the
    // protein.
    final int wellSpacedAtEndOfBreak;
    boolean lookAtBreak = proteinMightStillBeAttached & !inadvertentlyPushing;
    if (debug | lookAtBreak) {
      // if (proteinMightStillBeAttached & !inadvertentlyPushing) {
      // TODO: We should decide at the end if we want shapes or not.
      RefoldingCurve refoldingCurve = RefoldingUtil.analyseRefoldingCurve(
        refoldingResult, minPeakForceRelativeToEmpiricalBaselineN,
        medianAbsoluteDeviationLowerBoundN, leftMostlyHigherWindowSizeM, leftMostlyHigherFraction,
        leftMostlyHigherN, rightMuchHigherThanAllN, rightMuchHigherN, atMostOnePeakWindowM,
        fetchInfo.approachContactPointAndSlope.slopeZLsbDefLsb, svgIfInteresting, svgDefScale);
      boolean[] refoldingPeaksIndicator = refoldingCurve.peaksIndicator;
      // Allow for a wider range of inter-peak distance since we're not
      // fitting WLC (yet).
      if (lookAtBreak) {
        wellSpacedAtEndOfBreak = PicoDataAnalyser.wellSpacedAtEnd(
          CommonThings.toDoubleList(CommonThings.indicesOfTrue(refoldingPeaksIndicator)),
          clRangeStartM * 0.5, clRangeEndM * 1.5, allowSkipped);
        logger.log(Level.FINE, "wellSpacedAtEndOfBreak={0}", wellSpacedAtEndOfBreak);
      } else {
        wellSpacedAtEndOfBreak = 0;
      }
      short start = refoldingResult.retraction().sharedZArray()[0];
      // double startM = fetch.zLsbToMetresFactor
      // * (fetchInfo.retractionContactPoint.zLsb - start);
      // List<Shape> hoverNmText =
      // PicoShapes.drawHorizontalTextRightOf(PicoShapes.standardFont,
      // PicoShapes.standardFontRenderContext, String.format("%.3f nm",
      // startM * 1e9),
      // start, refoldingResult.retraction.sharedDeflectionArray()[0]);
      logger.log(
        Level.FINE,
        String.format(
          "hoverZDef.x=%s, hoverZDef.y=%s, start=%s, refoldingResult.retraction.sharedDeflectionArray()[0]=%s",
          hoverZDef.x, hoverZDef.y, start, refoldingResult.retraction().sharedDeflectionArray()[0]));
      shapes = new ArrayList<>();
      if (svgIfInteresting) {
        List<Shape> hoverNmText2 = PicoShapes.drawTextBelow(
          PicoShapes.standardFont, PicoShapes.standardFontRenderContext,
          String.format("%.3f nm", hoverDistanceM * 1e9), hoverZDef.x, hoverZDef.y * svgDefScale
            + 80);
        int m = 15;
        List<Shape> cross = ImmutableList.<Shape>of(new Line2D.Double(hoverZDef.x - m, hoverZDef.y
          * svgDefScale - m, hoverZDef.x + m, hoverZDef.y * svgDefScale + m), new Line2D.Double(
          hoverZDef.x - m, hoverZDef.y * svgDefScale + m, hoverZDef.x + m, hoverZDef.y
            * svgDefScale - m), ShapeUtilities.createTranslatedShape(
          ShapeUtilities.createRegularCross(m, m), hoverZDef.x, hoverZDef.y * svgDefScale));
        shapes.addAll(refoldingCurve.shapes);
        // shapes.addAll(hoverNmText);
        shapes.addAll(hoverNmText2);
        shapes.addAll(cross);
      }
    } else {
      wellSpacedAtEndOfBreak = 0;
      shapes = ImmutableList.of();
    }
    return new FetchRefoldBreakInfo(
      fetchInfo, approachRetractionPush.approachPushingFromDeflectionM,
      approachRetractionPush.retractionPushingM, hoverDistanceM, proteinMightStillBeAttached,
      wellSpacedAtEndOfBreak, shapes);
  }

  static FetchRefoldBreakInfo analysisOrNull(
    double svgDefScale, Path svgDir, double clRangeStartM, double clRangeEndM,
    FileContentAndTargetFile fileContentAndTargetPath, PlainPicoData refold,
    double minPeakForceRelativeToEmpiricalBaselineN, double medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    double rightMuchHigherThanAllN, double rightMuchHigherN, double atMostOnePeakWindowM,
    DoublePair hoverZDef, PeakAnalysisInput peakAnalysisInput, Path peakSlopesDir,
    ApproachRetractionPush approachRetractionPush, double linkerM, double linkerPerModuleM,
    int maxRequiredWellSpacedAtEnd, boolean debug) {
    FetchRefoldBreakInfo analysis;
    try {
      analysis = analyseFetchRefoldBreak(
        peakAnalysisInput, fileContentAndTargetPath.targetFile.getFileName().toString(),
        peakSlopesDir, approachRetractionPush, refold, minPeakForceRelativeToEmpiricalBaselineN,
        medianAbsoluteDeviationLowerBoundN, leftMostlyHigherWindowSizeM, leftMostlyHigherFraction,
        leftMostlyHigherN, rightMuchHigherThanAllN, rightMuchHigherN, atMostOnePeakWindowM,
        clRangeStartM, clRangeEndM, linkerM, linkerPerModuleM, svgDir != null, svgDefScale,
        hoverZDef, maxRequiredWellSpacedAtEnd, debug);
    } catch (IllegalArgumentException ex) {
      logger.log(
        Level.WARNING, "Could not analyseFetchRefoldBreak: {0} because of {1}.", new Object[] {
        fileContentAndTargetPath.targetFile, ex});
      analysis = null;
    }
    return analysis;
  }

  public static final Color REUNFOLDING_COLOR = new Color(47, 79, 47);

  private static final boolean DRAW_ZT_DT_SHAPES = false;

  static double process(
    Path junctionPointsDirectory, double slope, Path contactPointAndSlopeDir, Path peakSlopesDir,
    final double svgDefScale, final Path svgDir, Path infoDir, FetchRefoldBreakParams fetchRefoldBreakParams,
    DoubleList allowedTimeRelativeError,
    Function<? super Double, Double> minPeakForceN, double minPeakContourLengthM,
    double minContourLengthDeltaM, double minPeakForceRelativeToEmpiricalBaselineN,
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    Function<? super Double, Double> rightMuchHigherThanAllN,
    Function<? super Double, Double> rightMuchHigherN, double atMostOnePeakWindowM, double linkerM,
    double linkerPerModuleM, int maxRequiredWellSpacedAtEnd, double clRangeStartM,
    double clRangeEndM, double kbtOverP, boolean debug, PathMatcher debugPathMatcher,
    FileContentAndTargetFile fileContentAndTargetPath, Executor svgExecutor) throws IOException {
    PicoData picoData = PicoDataExtractor.parse(fileContentAndTargetPath.fileContent());
    final double pushingM;
    if (!(picoData instanceof StripChartPicoData)) {
      logger.log(Level.WARNING, "Not a strip chart file: {0}", fileContentAndTargetPath.targetFile());
      pushingM = Double.NaN;
    } else {
      final StripChartPicoData stripChart = (StripChartPicoData) picoData;
      final IntList a;
      if (junctionPointsDirectory == null) {
        a = findJunctionPoints(stripChart, fetchRefoldBreakParams, allowedTimeRelativeError);
      } else {
        Path junctionPointsFile = JpHelper.junctionPointsFile(
          junctionPointsDirectory, fileContentAndTargetPath.targetFile());
        a = JpHelper.readOrComputeAndWriteJunctionPoints(
          fetchRefoldBreakParams, stripChart, allowedTimeRelativeError, junctionPointsFile);
      }
      final ApproachRetractionLoop loop = (a == null || JpHelper.areJunctionPointsDefault(a)) ? null
        : createLoopGivenFetchRefoldBreakJunctionPoints(a, stripChart, fetchRefoldBreakParams);
      if (loop == null) {
        logger.log(
          Level.WARNING, "Could not extract the two iterations from: {0}",
          fileContentAndTargetPath.targetFile());
        pushingM = Double.NaN;
      } else {
        Preconditions.checkArgument(loop.iterations.length == 2);
        final PeakAnalysisInput peakAnalysisInput;
        PlainPicoData fetch = loop.iterations[0];
        // Cannot use lastRetraction because it usually does not even touch the surface.
        ContactPointsAndSlope contactPointsAndSlope = ContactPointAndSlopeHelper.readOrComputeAndWriteContactPointAndSlopeOrNullUsingApproachBaseline(
          fetch,
          slope, fileContentAndTargetPath.targetFile().getFileName().toString(),
          contactPointAndSlopeDir);
        peakAnalysisInput = contactPointsAndSlope == null ? null
          : new PeakAnalysisInput(
            fetch, contactPointsAndSlope, minPeakForceN.apply(fetch.sharedRetraction().velocity),
            minPeakContourLengthM, minContourLengthDeltaM,
            minPeakForceRelativeToEmpiricalBaselineN,
            medianAbsoluteDeviationLowerBoundN.apply(fetch.sharedRetraction().velocity),
            leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
            rightMuchHigherThanAllN.apply(fetch.sharedRetraction().velocity),
            rightMuchHigherN.apply(fetch.sharedRetraction().velocity), atMostOnePeakWindowM,
            kbtOverP);

        ApproachRetractionPush approachRetractionPush = peakAnalysisInput == null ? null
          : ApproachRetractionPush.computeApproachRetractionPush(peakAnalysisInput);
        if (approachRetractionPush == null) {
          pushingM = Double.NaN;
        } else {
          final String filename = fileContentAndTargetPath.targetFile().getFileName().toString();
          if (approachRetractionPush.retractionPushingM < approachRetractionPush.approachPushingFromDeflectionM + 50e-9) {
            pushingM = approachRetractionPush.retractionPushingM;
            logger.log(Level.INFO, "{0} pushed {1} nm on retraction.", new Object[] {
            filename, pushingM * 1e9});
          } else {
            logger.warning(String.format(
              "Retraction pushing %f nm is unreliable because it's too large compared to approach deflection pushing %f nm.",
              approachRetractionPush.retractionPushingM * 1e9,
              approachRetractionPush.approachPushingFromDeflectionM * 1e9));
            pushingM = approachRetractionPush.approachPushingFromDeflectionM;
          }
          if (svgDir != null || infoDir != null) {
            DoublePair hoverZDef = JpHelper.hoverZDef(a, stripChart);
            boolean debugPathMatches = debugPathMatcher.matches(fileContentAndTargetPath.targetFile());
            final FetchRefoldBreakInfo analysis = analysisOrNull(
              svgDefScale,
              svgDir,
              clRangeStartM,
              clRangeEndM,
              fileContentAndTargetPath,
              loop.iterations[1],
              minPeakForceRelativeToEmpiricalBaselineN,
              medianAbsoluteDeviationLowerBoundN.apply(loop.iterations[1].sharedRetraction().velocity),
              leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
              rightMuchHigherThanAllN.apply(loop.iterations[1].sharedRetraction().velocity),
              rightMuchHigherN.apply(loop.iterations[1].sharedRetraction().velocity),
              atMostOnePeakWindowM, hoverZDef, peakAnalysisInput, peakSlopesDir,
              approachRetractionPush, linkerM, linkerPerModuleM, maxRequiredWellSpacedAtEnd, debug & debugPathMatches);
            if (analysis != null) {
              if (analysis.isVeryInteresting()) {
                // It remains to be seen if they were in the
                // previously
                // stretched region.
                logger.log(
                  Level.INFO, "YES! Unfolded {0} modules during breaking. Path: {1}", new Object[] {
                  analysis.wellSpacedAtEndOfBreak - 1, fileContentAndTargetPath.targetFile()});
              }
              if (infoDir != null) {
                Path infoPath = infoDir.resolve(fileContentAndTargetPath.targetFile().getFileName()
                  + ".info");
                Files.write(
                  infoPath, (analysis.hoverDistanceM * 1e9 + "\n").getBytes(Charsets.ISO_8859_1));
              }
              if (svgDir != null & ((debug & debugPathMatches) | (DRAW_ZT_DT_SHAPES & analysis.isInteresting()))) {
                if (DRAW_ZT_DT_SHAPES) {
                  svgExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                      drawZtDtShapes(svgDir, stripChart, loop, filename);
                    }
                  });
                }
              }
              if (svgDir != null & !analysis.shapes.isEmpty()) {
                svgExecutor.execute(new Runnable() {
                  @Override
                  public void run() {
                    int k = loop.iterations.length * (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1);
                    List<Color> segmentColors = PicoShapes.colorsForPieces(1 + k);
                    List<Map<Color, List<Shape>>> maps = new ArrayList<>();
                    maps.add(ImmutableMap.of(segmentColors.get(0), analysis.shapes));
                    List<Path2D.Float> paths = PicoShapes.createPaths(
                      stripChart.sharedApproach().sharedZList(), stripChart.sharedApproach().sharedDeflectionList(),
                      loop.junctionPoints, svgDefScale, (short) 0, (short) 0);
                    for (int i = 0; i < paths.size(); ++i) {
                      Path2D.Float path = paths.get(i);
                      Color c = segmentColors.get(i % segmentColors.size());
                      maps.add(ImmutableMap.of(c, (List<Shape>) ImmutableList.<Shape>of(path)));
                    }
                    List<Shape> approachLines = PicoShapes.drawApproachContactPointAndSlope(
                      analysis.fetchInfo.approachContactPointAndSlope, analysis.fetchInfo.plainPicoData.sharedApproach(),
                      analysis.fetchInfo.plainPicoData.zLsbToMetresFactor, svgDefScale);
                    Color approachColor = segmentColors.get(0);
                    maps.add(ImmutableMap.of(approachColor, approachLines));
                    Map<Color, List<Shape>> nonPlainFetchShapes = PicoShapes.drawNonPlain(
                      analysis.fetchInfo.plainPicoData, analysis.fetchInfo.retractionContactPoint,
                      analysis.fetchInfo.offsetAndDeflections, analysis.fetchInfo.approachContactPointAndSlope.slopeZLsbDefLsb,
                      analysis.fetchInfo.peakOrNots, analysis.fetchInfo.wlcPeakForces(), analysis.fetchInfo.kbtOverP, svgDefScale);
                    maps.add(nonPlainFetchShapes);
                    Images.saveSvg(
                      CommonThings.unionMapsOfLists(maps),
                      svgDir.resolve(filename + "_" + analysis.isInteresting() + "_"
                        + analysis.isVeryInteresting() + ".svg"));
                  }
                });
              } else if (svgDir != null & ((debug & debugPathMatches) | analysis.isInteresting())) {
                svgExecutor.execute(new Runnable() {
                  @Override
                  public void run() {
                    Images.saveSvg(
                      analysis.fetchInfo, svgDefScale, svgDir.resolve(filename + "_fetch.svg"));
                  }
                });
              }
            }
          }
        }
      }
    }
    return pushingM;
  }

  private static void drawZtDtShapes(
    Path svgDir, StripChartPicoData stripChart, ApproachRetractionLoop loop, String filename) {
    List<Shape> ztShapes = PicoShapes.drawArrayAndJunctionPoints(
      loop.junctionPoints, stripChart.sharedApproach().sharedZList());
    Images.saveSvg(ztShapes, svgDir.resolve(filename + "_zt.svg"));
    List<Shape> dtShapes = PicoShapes.drawArrayAndJunctionPoints(
      loop.junctionPoints, stripChart.sharedApproach().sharedDeflectionList());
    Images.saveSvg(dtShapes, svgDir.resolve(filename + "_dt.svg"));
  }
}
